﻿// This source file is part of the atomdb project
// 
// Copyright (c) 2017 - 2022 pulsarware, All rights reserved.
// Copyright (c) 2017 - 2022 polarboy <polarboy@163.com>
// 
// See https://atomdb.com/LICENSE.txt for license information
// 
// Created by polarboy on 2023/12/04.

using System.Diagnostics;
using System.Text.RegularExpressions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace Pulsarware.ATOMDB.Packager.Windows;

public class WindowsHostDetector : AbstractHostDetector
{
   private readonly ILogger<WindowsHostDetector> _logger;
   
   public WindowsHostDetector(IOptions<Config> config, ILogger<WindowsHostDetector> logger)
      : base(config)
   {
      _logger = logger;
   }
   public override async Task<bool> DetectAsync()
   {
      _logger.LogInformation("Begin detecting build host environment ...");
      var detectCMakeRet = await DetectCMakeAsync();
      if (!detectCMakeRet)
      {
         return false;
      }
      
      var detectMakeRet = await DetectMakeAsync();
      if (!detectMakeRet)
      {
         return false;
      }

      var detectDeployQtRet = await DetectDeployQtAsync();
      if (!detectDeployQtRet)
      {
         return false;
      }
      
      var detectQtIfwRet = await DetectQtIfwAsync();
      if (!detectQtIfwRet)
      {
         return false;
      }
      return true;
   }

   private async Task<bool> DetectCMakeAsync()
   {
      var args = new [] { "--version" };
      var execResult = await ExecCommandAsync("cmake.exe", args);
      if (execResult.ExitCode != 0)
      {
         _logger.LogError("cmake command not found");
         return false;
      }

      var regexp = new Regex(@"cmake\s+version\s*(\d+\.\d+.\d+)(?:-[a-zA-Z0-9]{1,})?",
         RegexOptions.Compiled | RegexOptions.IgnoreCase);
      var match = regexp.Match(execResult.StdOutputMsg!);
      if (!match.Success)
      {
         _logger.LogError("Detect cmake version error");
         return false;
      }
      var detectedVersion = new Version(match.Groups[1].Value);
      if (detectedVersion < _config.MiniCMakeVersion)
      {
         _logger.LogError("Detect cmake version: {0}, but minimum required version: {1}",
            detectedVersion,
            _config.MiniCMakeVersion);
         return false;
      }

      var commandFullPath = findCommandPath("cmake.exe");
      if (commandFullPath is null)
      {
         _logger.LogError("Cmake command full path not found");
         return false;
      }

      _toolChain.CMakeBinary = commandFullPath;
      _logger.LogInformation("Found: {0} version: {1}", _toolChain.CMakeBinary, detectedVersion);
      return true;
   }

   private async Task<bool> DetectMakeAsync()
   {
      var ninjaBinaryPath = await findCommandPathAsync("ninja");
      var makeBinaryPath = await findCommandPathAsync("make");
      if (ninjaBinaryPath is null && makeBinaryPath is null)
      {
         _logger.LogError("Nina and Make both not found.");
         return false;
      }

      if (ninjaBinaryPath is not null)
      {
         _toolChain.NinjaBinary = ninjaBinaryPath;
      }

      if (makeBinaryPath is not null)
      {
         _toolChain.MakeBinary = makeBinaryPath;
      }

      if (_config.UseNinja && _toolChain.NinjaBinary is null)
      {
         _logger.LogError("Prefer ninja but ninja command not found.");
         return false;
      }

      if (_toolChain.NinjaBinary is not null)
      {
         var execResult = ExecCommandAsync("ninja", new []{"--version"});
         execResult.Wait();
         if (execResult.Result.ExitCode != 0)
         {
            _logger.LogError("Detect ninja version error, error: {0}", execResult.Result.StdErrorMsg);
            return false;
         }
         _logger.LogInformation("Found: {0} version: {1}", _toolChain.NinjaBinary, execResult.Result.StdOutputMsg!.Trim());
      }

      if (_toolChain.MakeBinary is not null)
      {
         var execResult = ExecCommandAsync("make", new []{"--version"});
         if (execResult.Result.ExitCode != 0)
         {
            _logger.LogError("Detect make version error.");
            return false;
         }
         var regexp = new Regex(@"GNU\s+Make\s*(\d+\.\d+[.\d+]?)",
            RegexOptions.Compiled | RegexOptions.IgnoreCase);
         var match = regexp.Match(execResult.Result.StdOutputMsg!);
         if (!match.Success)
         {
            _logger.LogError("Detect make version error");
            return false;
         }
         var detectedVersion = match.Groups[0].Value;
         _logger.LogInformation("Found: {0} version: {1}", _toolChain.MakeBinary, detectedVersion);
      }
      
      return true;
   }
   
   private async Task<bool> DetectDeployQtAsync()
   {
      // 先看看是否指定 SDK，指定了看里面的命令是否有对应的命令
      string deployQtBinary;
      string targetVersion = String.Empty;
      if (_config.QtDir is not null && _config.QtDir.Exists)
      {
         string? qtSDKDir = DetectQtSDK();
         if (qtSDKDir is null)
         {
            return false;
         }
         deployQtBinary = Path.Combine(
            qtSDKDir,
            "mingw_64",
            "bin",
            "windeployqt6.exe");
      }
      else
      {
         // 自行查找
         var deployQtBinaryRet = await findCommandPathAsync("windeployqt6.exe");
         if (deployQtBinaryRet is null)
         {
            _logger.LogError("windeployqt6.exe is not found");
            return false;
         }
         // 这里需要判断版本号
         var execRet = await ExecCommandAsync(deployQtBinaryRet, new [] {"--version"}); 
         var deployQtVersionRegex = new Regex(@"Qt Deploy Tool (\d\.\d\.\d)",
            RegexOptions.Compiled | RegexOptions.IgnoreCase);
         var match = deployQtVersionRegex.Match(execRet.StdOutputMsg!);
         if (!match.Success)
         {
            _logger.LogError("Detect windeployqt6 version error");
            return false;
         }
         
         var deployVersionStr = match.Groups[1].Value;
         var detectVersion = new Version(deployVersionStr);
         if (detectVersion < _config.QtVersion)
         {
            _logger.LogError("Detect windeployqt6 versions: [{0}, but minimum required version: {1}",
               detectVersion,
               _config.QtVersion);
         }
         
         targetVersion = deployVersionStr;
         deployQtBinary = deployQtBinaryRet;
      }
      _toolChain.DeployQtBinary = deployQtBinary;
      _logger.LogInformation("Found: {0} version: {1}", deployQtBinary, targetVersion);
      return true;
   }

   private string? DetectQtSDK()
   {
      var qtDir = _config.QtDir!;
      var versionStrList = new List<string>();
      var versions = new List<Version>();
      foreach (var dir in qtDir.EnumerateDirectories())
      {
         if (char.IsDigit(dir.Name.First()) && char.IsDigit(dir.Name.Last()))
         {
            versions.Add(new Version(dir.Name));
            versionStrList.Add(dir.Name);
         }
      }

      if (versions.Count == 0)
      {
         _logger.LogError("No Qt version available");
         return null;
      }
      versions.Sort();
      var miniQtVersion = _config.QtVersion;
      var requireVersionCheckPassed = false;
      var checkedVersions = new List<Version>();
      foreach (var version in versions)
      {
         if (version > miniQtVersion)
         {
            requireVersionCheckPassed = true;
            checkedVersions.Add(version);
         }
      }

      if (!requireVersionCheckPassed)
      {
         _logger.LogError("Detect Qt SDK versions: {0}, but minimum required version: {1}",
            string.Join(",", versionStrList),
            miniQtVersion);
         return null;
      }

      return Path.Combine(qtDir.FullName, versionStrList.Last());
   }

   /// <summary>
   /// 探测指定框架里面的 Qt Installer Framework
   /// </summary>
   /// <returns></returns>
   private string? DetectQtSDKIfw()
   {
      var qtDir = _config.QtDir!;
      var versionStrList = new List<string>();
      var versions = new List<Version>();
      var ifwInstallerDir = new DirectoryInfo(Path.Combine(qtDir.FullName, "Tools", "QtInstallerFramework"));
      foreach (var dir in ifwInstallerDir.EnumerateDirectories())
      {
         if (char.IsDigit(dir.Name.First()) && char.IsDigit(dir.Name.Last()))
         {
            versions.Add(new Version(dir.Name));
            versionStrList.Add(dir.Name);
         }
      }

      if (versions.Count == 0)
      {
         _logger.LogError("No QtInstallerFramework version available");
         return null;
      }
      versions.Sort();
      var miniIfwVersion = _config.MiniIfwVersion;
      var requireVersionCheckPassed = false;
      var checkedVersions = new List<Version>();
      foreach (var version in versions)
      {
         if (version > miniIfwVersion)
         {
            requireVersionCheckPassed = true;
            checkedVersions.Add(version);
         }
      }

      if (!requireVersionCheckPassed)
      {
         _logger.LogError("Detect Qt QtInstallerFramework versions: {0}, but minimum required version: {1}",
            string.Join(",", versionStrList),
            miniIfwVersion);
         return null;
      }

      return Path.Combine(ifwInstallerDir.FullName, versionStrList.Last());
   }
   
   private async Task<bool> DetectQtIfwAsync()
   {
      // 先看看是否指定 SDK，指定了看里面的命令是否有对应的命令
      string ifwBinary;
      if (_config.QtDir is not null && _config.QtDir.Exists)
      {
         string? qtIfwDir = DetectQtSDKIfw();
         if (qtIfwDir is null)
         {
            return false;
         }
         ifwBinary = Path.Combine(
            qtIfwDir,
            "bin",
            "binarycreator.exe");
      }
      else
      {
         // 自行查找
         var qtIfwBinaryRet = await findCommandPathAsync("binarycreator.exe");
         if (qtIfwBinaryRet is null)
         {
            _logger.LogError("binarycreator.exe is not found");
            return false;
         }
         ifwBinary = qtIfwBinaryRet;
      }

      _toolChain.QtIfwBinary = ifwBinary;
      _logger.LogInformation("Found: {0}", ifwBinary);
      return true;
   }
   
   protected override string? findCommandPath(string command)
   {
      var execRet = ExecCommandAsync("where.exe", new[] { command });
      execRet.Wait();
      string? message;
      if (execRet.Result.ExitCode != 0)
      {
         message = execRet.Result.StdErrorMsg is not null ? execRet.Result.StdErrorMsg!.Trim() : null;
      }
      else
      {
         message = execRet.Result.StdOutputMsg is not null ? execRet.Result.StdOutputMsg!.Trim() : null;
         if (message is not null && message.IndexOf(Environment.NewLine) != -1)
         {
            message = message.Split(Environment.NewLine).First();
         }
      }
     
      return execRet.Result.ExitCode == 0 ? message : null;
   }

   protected override async Task<string?> findCommandPathAsync(string command)
   {
      var execRet = await ExecCommandAsync("where.exe", new[] { command });
      string? message;
      if (execRet.ExitCode != 0)
      {
         message = execRet.StdErrorMsg is not null ? execRet.StdErrorMsg!.Trim() : null;
      }
      else
      {
         message = execRet.StdOutputMsg is not null ? execRet.StdOutputMsg!.Trim() : null;
      }
      return execRet.ExitCode == 0 ? message : null;
   }
}